package com.google.code.v8config;

import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;

/**
 *
 * @author yvad
 */
public final class Archiver {
    
    //--------------------------------------------------------------------------
    private static final int EMPTY_ADDRESS = 0x7fffffff;

    private static final class PackedIO {
        public static byte readByte(byte[] buffer, int position) {
            return buffer[position];
        }
        public static void writeByte(byte[] buffer, int position, byte value) {
            buffer[position] = value;
        }

        public static short readShort(byte[] buffer, int position) {
            return (short)((buffer[position]&0xff)+((buffer[position+1]&0xff)<<8));
        }
        public static void writeShort(byte[] buffer, int position, short value) {
            buffer[position] = (byte)(value&0xff);
            buffer[position+1] = (byte)((value>>>8)&0xff);
        }

        public static int readInt(byte[] buffer, int position) {
            return (buffer[position]&0xff)
                    +((buffer[position+1]&0xff)<<8)
                    +((buffer[position+2]&0xff)<<16)
                    +((buffer[position+3]&0xff)<<24);
        }
        public static void writeInt(byte[] buffer, int position, int value) {
            buffer[position] = (byte)(value&0xff);
            buffer[position+1] = (byte)((value>>>8)&0xff);
            buffer[position+2] = (byte)((value>>>16)&0xff);
            buffer[position+3] = (byte)((value>>>24)&0xff);
        }

        public static int readHexStringInt(byte[] buffer, int position) 
                throws IOException {
            
            int value = 0;
            
            for(int i=0; i<8; i++) {
                int k = buffer[position+i];

                if(k>='0' && k<='9')
                    k = k - '0';
                else if(k>='a' && k<='f')
                    k = k-'a' + 10;
                else if(k>='A' && k<='F')
                    k = k-'A' + 10;
                else
                    throw new IOException("Illegal symbol");

                value = (value<<4) + k;
            }

            return value;
        }
        public static void writeHexStringInt(byte[] buffer, int position, int value)
                throws IOException {        
            
            for(int i=0; i<8; i++) {
                int k = (value>>>((8-1-i)*4))&0xf;

                int code = 0;
                if(k>=0 && k<=9)
                    code = k + '0';
                else if(k>=10 && k<=16)
                    code = k-10 + 'a';
                else
                    throw new IOException("Illegal value");

                buffer[position+i] = (byte)code;
            }

        }

        public static long readLong(byte[] buffer, int position) {
            int low = readInt(buffer, position);
            long hi = readInt(buffer, position+4)&0xffffffff;
            return (hi<<32)+low;
        }
        public static void writeLong(byte[] buffer, int position, long value) {
            writeInt(buffer, position, (int)value&0xffffffff);
            writeInt(buffer, position+4, (int)(value>>>32)&0xffffffff);
        }

        public static String readString(byte[] buffer, int position) {
            StringBuilder builder = new StringBuilder();

            for(int i=0; ; i++) {
                int codepoint = PackedIO.readShort(buffer, position+i*2);
                if(codepoint==0)
                    break;
                
                builder.appendCodePoint(codepoint);
            }

            return builder.toString();
        }
        public static void writeString(byte[] buffer, int position, String value) {
            for(int i=0; i<value.length(); i++) 
                writeShort(buffer, position+i*2, (short)value.codePointAt(i));
            writeInt(buffer, position+value.length()*2, 0);
        }
    }
    
    private static final class PackedFileHeader {

        //----------------------------------------------------------------------
        public int nextAddress; // Is always 0x7fffffff?
        public int blockSize; // Is always 0x200?
        public int storageVersion;
        public int reserved0; // Is always 0?

        //----------------------------------------------------------------------
        public int length() {
            return 16;
        }
        public int read(byte[] buffer, int position)
                throws IOException {

            nextAddress = PackedIO.readInt(buffer, position);
            blockSize = PackedIO.readInt(buffer, position+4);
            storageVersion = PackedIO.readInt(buffer, position+8);
            reserved0 = PackedIO.readInt(buffer, position+12);

            if(blockSize!=0x200 || reserved0!=0x0)
                throw new IOException("Wrong header format");

            return position+length();
        }
        public int write(byte[] buffer, int position)
                throws IOException {

            PackedIO.writeInt(buffer, position, nextAddress);
            PackedIO.writeInt(buffer, position+4, blockSize);
            PackedIO.writeInt(buffer, position+8, storageVersion);
            PackedIO.writeInt(buffer, position+12, reserved0);

            return position+length();
        }

        //----------------------------------------------------------------------
    }
    private static final class PackedBlockHeader {

        //----------------------------------------------------------------------
        protected final byte[] BOUNDS = {0x0D, 0x0A, 0x20};

        //----------------------------------------------------------------------
        public int dataSize;
        public int blockSize;
        public int nextAddress;

        //----------------------------------------------------------------------
        public int length() {
            return 31;
        }
        public int read(byte[] buffer, int position)
                throws IOException {

            if(PackedIO.readByte(buffer, position)!=BOUNDS[0]
                    || PackedIO.readByte(buffer, position+1)!=BOUNDS[1]
                    || PackedIO.readByte(buffer, position+10)!=BOUNDS[2]
                    || PackedIO.readByte(buffer, position+19)!=BOUNDS[2]
                    || PackedIO.readByte(buffer, position+28)!=BOUNDS[2]
                    || PackedIO.readByte(buffer, position+29)!=BOUNDS[0]
                    || PackedIO.readByte(buffer, position+30)!=BOUNDS[1])
                throw new IOException("Wrong block header");

            dataSize = PackedIO.readHexStringInt(buffer, position+2);
            blockSize = PackedIO.readHexStringInt(buffer, position+11);
            nextAddress = PackedIO.readHexStringInt(buffer, position+20);

            return position+length();
        }
        public int write(byte[] buffer, int position)
                throws IOException {

            PackedIO.writeByte(buffer, position, BOUNDS[0]);
            PackedIO.writeByte(buffer, position+1, BOUNDS[1]);
            PackedIO.writeHexStringInt(buffer, position+2, dataSize);
            PackedIO.writeByte(buffer, position+10, BOUNDS[2]);
            PackedIO.writeHexStringInt(buffer, position+11, blockSize);
            PackedIO.writeByte(buffer, position+19, BOUNDS[2]);
            PackedIO.writeHexStringInt(buffer, position+20, nextAddress);
            PackedIO.writeByte(buffer, position+28, BOUNDS[2]);
            PackedIO.writeByte(buffer, position+29, BOUNDS[0]);
            PackedIO.writeByte(buffer, position+30, BOUNDS[1]);

            return position+length();
        }

        //----------------------------------------------------------------------
    }
    private static final class PackedElementAddress {

        //----------------------------------------------------------------------
        public int headerAddress;
        public int dataAddress;
        public int nextAddress; // Is always 0x7fffffff?

        //----------------------------------------------------------------------
        public int length() {
            return 12;
        }
        public int read(byte[] buffer, int position)
                throws IOException {

            headerAddress = PackedIO.readInt(buffer, position);
            dataAddress = PackedIO.readInt(buffer, position+4);
            nextAddress = PackedIO.readInt(buffer, position+8);

            return position+length();
        }
        public int write(byte[] buffer, int position)
                throws IOException {
            
            PackedIO.writeInt(buffer, position, headerAddress);
            PackedIO.writeInt(buffer, position+4, dataAddress);
            PackedIO.writeInt(buffer, position+8, nextAddress);

            return position+length();
        }

        //----------------------------------------------------------------------
    }
    private static final class PackedElementHeader {

        //----------------------------------------------------------------------
        public long creationDate;
        public long modificationDate;
        public int reserved0;
        public String name = "";

        //----------------------------------------------------------------------
        public int length() {
            return 20+name.length()*2+4;
        }
        public int read(byte[] buffer, int position)
                throws IOException {

            creationDate = PackedIO.readLong(buffer, position);
            modificationDate = PackedIO.readLong(buffer, position+8);
            reserved0 = PackedIO.readInt(buffer, position+16);
            name = PackedIO.readString(buffer, position+20);

            if(reserved0!=0)
                throw new IOException("Illegal value");

            return position+length();
        }
        public int write(byte[] buffer, int position)
                throws IOException {

            PackedIO.writeLong(buffer, position, creationDate);
            PackedIO.writeLong(buffer, position+8, modificationDate);
            PackedIO.writeInt(buffer, position+16, reserved0);
            PackedIO.writeString(buffer, position+20, name);

            return position+length();
        }

        //----------------------------------------------------------------------
    }
    
    //--------------------------------------------------------------------------
    private Archiver() {
        throw new UnsupportedOperationException();
    }
    
    //--------------------------------------------------------------------------
    public static void pack(String dirname, String filename) 
            throws IOException {
        
        if(dirname==null)
            throw new IOException("Empty dirname");
        if(filename==null)
            throw new IOException("Empty filename");
        
        File file = new File(filename);
        File dir = new File(dirname);
        if(!dir.exists())
            throw new IOException("Directory <"+dirname+"> not found");
        
        byte[] buffer = pack(dirname, 0);
        FileUtils.writeFile(file, buffer);
        
    }
    public static void unpack(String filename, String dirname) 
            throws IOException {
        
        if(filename==null)
            throw new IOException("Empty filename");
        if(dirname==null)
            throw new IOException("Empty dirname");
        
        File file = new File(filename);
        if(!file.exists())
            throw new IOException("File <"+filename+"> not found");

        File dir = new File(dirname);
        if(dir.exists())
            FileUtils.removeDirComponents(dir);
        else
            dir.mkdirs();
        
        byte[] buffer = FileUtils.readFileAsBytes(file);
        unpack(dirname, buffer, 0);
            
    }
    
    //--------------------------------------------------------------------------
    
    private static byte[] pack(String dirname, int level) 
            throws IOException {
        
        File dir = new File(dirname);
        File[] files = dir.listFiles();
        
        PackedFileHeader fileHeader = new PackedFileHeader();
        PackedBlockHeader blockHeader = new PackedBlockHeader();
        PackedElementAddress elementAddress = new PackedElementAddress();
        PackedElementHeader elementHeader = new PackedElementHeader();
        PackedBlockHeader blockHeaderHeader = new PackedBlockHeader();
        PackedBlockHeader blockHeaderData = new PackedBlockHeader();

        byte[] fileHeaderBuffer = new byte[fileHeader.length()];
        fileHeader.nextAddress = EMPTY_ADDRESS;
        fileHeader.blockSize = 0x200;
        fileHeader.storageVersion = 1;
        fileHeader.reserved0 = 0;
        fileHeader.write(fileHeaderBuffer, 0);

        int elementAddressBufferSize = elementAddress.length()*files.length;
        byte[] blockHeaderBuffer = new byte[blockHeader.length()];
        blockHeader.dataSize = elementAddressBufferSize;
        blockHeader.blockSize = elementAddressBufferSize;
        blockHeader.nextAddress = EMPTY_ADDRESS;
        blockHeader.write(blockHeaderBuffer, 0);

        int elementsStartPosition = fileHeaderBuffer.length + blockHeaderBuffer.length + elementAddressBufferSize;
        byte[] elementAddressBuffer = new byte[elementAddressBufferSize];
        int elementAddressBufferPosition = 0;
        List<byte[]> elementsBufferList = new LinkedList<byte[]>();
        int elementsBufferListSize = 0;
        for(File file : files) {
            //System.out.println(file.getName());
           
            byte[] data = file.isDirectory() ? pack(file.getPath(), level+1) : FileUtils.readFileAsBytes(file);
            if(level==0)
                data = compressBuffer(data);

            elementHeader.creationDate = 0;
            elementHeader.modificationDate = 0;
            elementHeader.reserved0 = 0;
            elementHeader.name = file.getName();
            blockHeaderHeader.dataSize = elementHeader.length();
            blockHeaderHeader.blockSize = blockHeaderHeader.dataSize;
            blockHeaderHeader.nextAddress = EMPTY_ADDRESS;
            
            int elementBufferSize = blockHeaderHeader.length() + elementHeader.length();

            blockHeaderData.dataSize = data.length;
            blockHeaderData.blockSize = blockHeaderData.dataSize;
            blockHeaderData.nextAddress = EMPTY_ADDRESS;

            elementBufferSize += blockHeaderData.length() + data.length;

            byte[] elementBuffer = new byte[elementBufferSize];
            int elementBufferPosition = 0;

            elementAddress.headerAddress = elementsStartPosition + elementsBufferListSize + elementBufferPosition;
            elementBufferPosition = blockHeaderHeader.write(elementBuffer, elementBufferPosition);
            elementBufferPosition = elementHeader.write(elementBuffer, elementBufferPosition);

            elementAddress.dataAddress = elementsStartPosition + elementsBufferListSize + elementBufferPosition;
            elementBufferPosition = blockHeaderData.write(elementBuffer, elementBufferPosition);
            System.arraycopy(data, 0, elementBuffer, elementBufferPosition, data.length);

            elementsBufferList.add(elementBuffer);
            elementsBufferListSize += elementBuffer.length;

            elementAddress.nextAddress = EMPTY_ADDRESS;
            elementAddressBufferPosition = elementAddress.write(elementAddressBuffer, elementAddressBufferPosition);
            
        }

        int bufferSize = fileHeaderBuffer.length + blockHeaderBuffer.length
                + elementAddressBufferSize + elementsBufferListSize;
        byte[] buffer = new byte[bufferSize];
        int bufferPosition = 0;

        System.arraycopy(fileHeaderBuffer, 0, buffer, bufferPosition, fileHeaderBuffer.length);
        bufferPosition += fileHeaderBuffer.length;

        System.arraycopy(blockHeaderBuffer, 0, buffer, bufferPosition, blockHeaderBuffer.length);
        bufferPosition += blockHeaderBuffer.length;

        System.arraycopy(elementAddressBuffer, 0, buffer, bufferPosition, elementAddressBuffer.length);
        bufferPosition += elementAddressBuffer.length;

        for(byte[] elementBuffer : elementsBufferList) {
            System.arraycopy(elementBuffer, 0, buffer, bufferPosition, elementBuffer.length);
            bufferPosition += elementBuffer.length;
        }

        return buffer;
    }
    private static void unpack(String dirname, byte[] buffer, int level) 
            throws IOException {
        
        PackedFileHeader fileHeader = new PackedFileHeader();
        PackedElementAddress elementAddress = new PackedElementAddress();
        PackedElementHeader elementHeader = new PackedElementHeader();
        int bufferPosition = 0;
        
        // read file header
        bufferPosition = fileHeader.read(buffer, bufferPosition);
        if(bufferPosition==buffer.length)
            return;

         while(true) {
             
            byte[] elementAddressListBuffer = readBlockHeaderList(buffer, bufferPosition);
            int elementAddressCount = elementAddressListBuffer.length / elementAddress.length();
            int elementAddressListBufferPosition = 0;
            for(int elementIndex=0; elementIndex<elementAddressCount; elementIndex++) {
                elementAddressListBufferPosition = elementAddress.read(elementAddressListBuffer, elementAddressListBufferPosition);

                byte[] elementAddressBuffer = readBlockHeaderList(buffer, elementAddress.headerAddress);
                elementHeader.read(elementAddressBuffer, 0);
                //System.out.println(elementHeader.name);

                if(elementAddress.dataAddress!=EMPTY_ADDRESS) {
                    byte[] elementDataBuffer = readBlockHeaderList(buffer, elementAddress.dataAddress);
                    byte[] innerBuffer = level==0 ? decompressBuffer(elementDataBuffer) : elementDataBuffer;

                    String filename = dirname + FileUtils.FILE_SEPARATOR + elementHeader.name;
                    if(isNested(innerBuffer)) {
                        
                        File dir = new File(filename);
                        dir.mkdir();
                        
                        unpack(filename, innerBuffer, level+1);
                        
                    } else 
                        FileUtils.writeFile(new File(filename), innerBuffer);
                }
            }
            
            if(fileHeader.nextAddress!=EMPTY_ADDRESS) 
                fileHeader.nextAddress = EMPTY_ADDRESS;
            else
                break;
         }
    }
    
    private static byte[] readBlockHeaderList(byte[] buffer, int bufferPosition)
            throws IOException {

        PackedBlockHeader blockHeader = new PackedBlockHeader();
        bufferPosition = blockHeader.read(buffer, bufferPosition);
        byte[] blockBuffer = new byte[blockHeader.dataSize];

        int i = 0;
        while(true) {
            int size = Math.min(blockBuffer.length-i, blockHeader.blockSize);
            System.arraycopy(buffer, bufferPosition, blockBuffer, i, size);
            i += size;
            
            if(i==blockBuffer.length || blockHeader.nextAddress==EMPTY_ADDRESS)
                break;

            bufferPosition = blockHeader.read(buffer, blockHeader.nextAddress);
        }
        if(i!=blockBuffer.length || blockHeader.nextAddress!=EMPTY_ADDRESS)
            throw new IOException("Buffer is not fully read");
        
        return blockBuffer;
    }
    
    private static boolean isNested(byte[] buffer) {

        PackedFileHeader fileHeader = new PackedFileHeader();

        if(buffer.length<fileHeader.length())
            return false;

        try {
            
            fileHeader.read(buffer, 0);
            
        } catch(IOException e) {
            return false;
        }

        return true;
    }
    
    private static byte[] decompressBuffer(byte[] buffer)
            throws IOException {
        
        final int INNER_BUFFER_SIZE = 4000;
        List<byte[]> innerBufferList = new LinkedList<byte[]>();
        int decompressedSize = 0;
        
        Inflater decompresser = new Inflater(true);
        decompresser.setInput(buffer);
        while(!decompresser.finished()) {
            try {
                
                byte[] innerBuffer = new byte[INNER_BUFFER_SIZE];
                int decompressed = decompresser.inflate(innerBuffer);
                if(decompressed==0)
                    throw new IOException("Decompresser needs more data");
                innerBufferList.add(innerBuffer);
                decompressedSize += decompressed;
                
            } catch(DataFormatException e) {
                throw new IOException(e);
            }
        }
        decompresser.end();
        
        byte[] outputBuffer = new byte[decompressedSize];
        int innerBufferPosition = 0;        
        for(byte[] inflateBuffer : innerBufferList) {
            int remainedBufferSize = decompressedSize - innerBufferPosition;
            int bufferSize = remainedBufferSize>INNER_BUFFER_SIZE ? INNER_BUFFER_SIZE : remainedBufferSize;
            System.arraycopy(inflateBuffer, 0, outputBuffer, innerBufferPosition, bufferSize);
            innerBufferPosition += bufferSize;
        }

        return outputBuffer;
    }
    private static byte[] compressBuffer(byte[] buffer)
            throws IOException {
        
        byte[] innerBuffer = new byte[(buffer.length>100 ? buffer.length : 100)];
        
        Deflater compresser = new Deflater(Deflater.BEST_COMPRESSION, true);
        compresser.setInput(buffer);
        compresser.finish();

        int compressedSize = compresser.deflate(innerBuffer);
        if(!compresser.finished())
            throw new IOException("Compresser not finished");
        compresser.end();

        byte[] outputBuffer = new byte[compressedSize];
        System.arraycopy(innerBuffer, 0, outputBuffer, 0, compressedSize);

        return outputBuffer;
    }
    
    //--------------------------------------------------------------------------
}
